
package scanner

import (
  "bufio"
  "unicode"
)

// Constants for tokens
const (
  None byte = iota
  Number  // Digit{Digit}
  Ident   // Letter{Letter|Digit}
  Add     // '+'
  Sub     // '-'
  Mul     // '*'
  Div     // '/'
  LPar    // '('
  RPar    // ')'
  Semic   // ';'
  Period  // '.'
  Equal   // '='
  Print   // 'print'
  Input   // 'input'
  Eos
)

var keywords = map[string]byte {
  "print": Print,
  "input": Input }

// Scanner contains array of characters and current position
type Scanner struct {
  source *bufio.Reader
  line int
}

func New(src *bufio.Reader) *Scanner {
  return &Scanner{src, 1}
}

func (s *Scanner) peek() rune {
  br, ok := s.source.Peek(1)
  if ok != nil { return 0 }
  return rune(br[0])
}

func (s *Scanner) char() rune {
  ch, _, ok := s.source.ReadRune()
  if ok != nil { return 0 }
  return ch
}

// Helper method to scan characters with given predicate
func (s* Scanner) scanWith(predicate func(r rune) bool) string {
  res := ""
  ch := s.char()
  for predicate(ch) {
    res += string(ch)
    if ch == '\n' { s.line++ }
    ch = s.char()
  }
  s.source.UnreadRune()
  return res
}

func isLetterOrDigit(r rune) bool {
  return unicode.IsLetter(r) || unicode.IsDigit(r)
}

// Scan new lexeme and return it with token
func (s *Scanner) Next() (string, byte) {
  // skip whitespaces
  s.scanWith(unicode.IsSpace)

  ch := s.peek()
  if ch == 0 { return "", Eos }

  // Scan numbers
  if unicode.IsDigit(ch) {
    return s.scanWith(unicode.IsDigit), Number
  }

  // identifiers
  if unicode.IsLetter(ch) {
    iden := s.scanWith(isLetterOrDigit)
    tok, iskey := keywords[iden]
    if !iskey { tok = Ident }
    return iden, tok
  }

  // scan symbols
  ch = s.char()
  var res byte = None
  switch ch {
    case '+': res = Add
    case '-': res = Sub
    case '*': res = Mul
    case '/': res = Div
    case '(': res = LPar
    case ')': res = RPar
    case '=': res = Equal
    case '?': res = Print
    case ';': res = Semic
    case '.': res = Period
  }

  return string(ch), res
}

